/*-*-C-*-
 *
 * Drag-n-drop
 *
 * This module implements the "sender" side of drag-n-drop, as well
 * as some handy functions for the "receiver" side to use.  The
 * "receiver" side is mostly handled by individual code for document,
 * etc.
 */

#include "resed.h"

#include "swicall.h"
#include "wimp.h"

#include "dragdrop.h"
#include "interactor.h"


static Bool truesize;
static RectRec dragbbox;            /* pointer-relative, millipoints */
static DragDropCallback callback;
static Bool dragfinished;

static DragDropCallbackRec dd;


/*
 ****************************************************************
 * UTILITIES
 ****************************************************************
 */


/*
 * Display the "scrolling" pointer shape
 */

void dragdrop_scroll_pointer ()
{
    (void) swi (Wimp_SpriteOp,  R0, 36,  R2, "ptr_scroll",  R3, 2,  R4, 12,  R5, 12,  R6, 0,  R7, 0,  END);
}


/*
 * Reset the mouse pointer to its default
 */

void dragdrop_normal_pointer ()
{
    (void) swi (Wimp_SetPointerShape,  R0, 1,  R1, -1,  END);
}


/*
 * Nudge the mouse pointer by n OS units in the direction indicated by the
 *  key 'keycode'.
 *
 * Call this from within an interactor when a key press is received; it
 *  will return TRUE iff the key was used.
 */

Bool dragdrop_nudge (int keycode, int n)
{
    /* cursor keys nudge the pointer */
    if (keycode >= 0x18c && keycode <= 0x18f)
    {
        char pb[5];    /* parameter block for OS_Word 21 */
        int v;

        /* read pointer position */
        pb[0] = 6;
        swi (OS_Word, R0, 21, R1, pb, END);

        /* update by n OS units in requisite direction */
        switch (keycode)
        {
        case 0x18c:   /* left arrow */
            v = pb[1] - n;
            if (v < 0)
                pb[2]--;
            pb[1] = v;
            break;
        case 0x18d:   /* right arrow */
            v = pb[1] + n;
            if (v >= 256)
                pb[2]++;
            pb[1] = v;
            break;
        case 0x18e:   /* down arrow */
            v = pb[3] - n;
            if (v < 0)
                pb[4]--;
            pb[3] = v;
            break;
        case 0x18f:   /* up arrow */
            v = pb[3] + n;
            if (v >= 256)
                pb[4]++;
            pb[3] = v;
            break;
        }

        /* write mouse position */
        pb[0] = 3;
        swi (OS_Word, R0, 21, R1, pb, END);

        return TRUE;
    }

    return FALSE;
}



/*
 ****************************************************************
 * SENDER
 ****************************************************************
 */


/*
 * Do a dragbox, using the millipoint-specified
 * pointer-relative box 'dragbbox'.  Handles types 0, 5 and 7.
 *
 * New version of do_dragbox (09-May-1994) by Matt Cloke
 *   version 1.00 MCC
 *
 * On Entry: type = type of drag <un-changed>
 *           *sprite_area = pointer to a sprite area, if null revert to old
 *                           style
 *           *sprite_name = pointer to a name of a sprite, if null revert to
 *                           old style
 */

static error * do_dragbox (int type, int *sprite_area, char *sprite_name)
{
    int solid = 0;

    /* read CMOS bit */
    ER( swi (OS_Byte,  R0, 161,  R1, 0x1c,  OUT,  R2, &solid,  END) );
    solid = solid & 0x02;
/*    dprintf("solid = %d\n",solid); */


    if (type == 0)
    {
      if ( sprite_area == NULL || sprite_name == NULL || solid == 0)
      {
          return swi (Wimp_DragBox,  R1, 0,  END);
      }
      else
      {
          return swi(DragASprite_Stop, END);
      }

    }

    if (type == 5 || type == 7)
    {
        PointerInfoRec mouse;
        DragBoxRec box;

        ER ( swi(Wimp_GetPointerInfo,  R1, &mouse,  END) );
        box.type = type;
        box.initial.minx = dragbbox.minx / 400 + mouse.position.x;
        box.initial.maxx = dragbbox.maxx / 400 + mouse.position.x;
        box.initial.miny = dragbbox.miny / 400 + mouse.position.y;
        box.initial.maxy = dragbbox.maxy / 400 + mouse.position.y;
        box.constrain.minx = box.constrain.miny = -1000000;
        box.constrain.maxx = box.constrain.maxy =  1000000;
        if (sprite_area == NULL || sprite_name == NULL || solid == 0)
        {
            return swi (Wimp_DragBox,  R1, &box,  END);
        }
        else
        {
            box.initial.minx = -(dragbbox.maxx -dragbbox.minx) / 400 +
                                                        mouse.position.x;
            box.initial.maxx =  (dragbbox.maxx -dragbbox.minx) / 400 +
                                                        mouse.position.x;
            box.initial.miny = -(dragbbox.maxy -dragbbox.miny) / 400 +
                                                        mouse.position.y;
            box.initial.maxy =  (dragbbox.maxy -dragbbox.miny) / 400 +
                                                        mouse.position.y;
            return swi (DragASprite_Start, R0, 0xc5,
                                           R1, sprite_area,
                                           R2, sprite_name,
                                           R3, &(box.initial), END);
        }
    }
    return NULL;
}


/*
 * Interactor for drag and drop
 */

static error * dd_interactor (unsigned int event, int *buf, void *closure, Bool *consumed)
{
    int messbuf[64];
    MessageDraggingPtr msg = (MessageDraggingPtr) messbuf;

    if (buf == NULL)            /* forcibly cancelled */
    {
        /* If there is a claimant, inform it that we're backing out */

/* dprintf("Aborting drag "); */
        if (dd.claimant != -1)
        {
/* dprintf("& telling claimant "); */
            msg->header.yourref = dd.claimantsref;
            msg->header.messageid = MESSAGE_DRAGGING;
            msg->flags = BIT(4);
            msg->header.size = sizeof (MessageDraggingRec);
            (void) swi (Wimp_SendMessage,
                        R0, EV_USER_MESSAGE,
                        R1, msg,
                        R2, dd.claimant,  END);
            dd.claimant = -1;

            if (dd.claimantsflags & BIT(0))
                dragdrop_normal_pointer ();
        }
/* dprintf("\n"); */
        return do_dragbox (0, NULL, NULL);
    }

/* dprintf("dd_interactor in "); */
    switch (event)
    {
    case EV_NULL_REASON_CODE:
        {
            *consumed = TRUE;
/* dprintf("NULL "); */
            ER ( swi (Wimp_GetPointerInfo,  R1, &dd.mouse,  END) );
            msg->header.yourref = (dd.claimant == -1 ? 0 : dd.claimantsref);
            msg->header.messageid = MESSAGE_DRAGGING;
            msg->windowhandle = dd.mouse.windowhandle;
            msg->iconhandle = dd.mouse.iconhandle;
            msg->position = dd.mouse.position;
            msg->flags = dd.dragflags;
            if (truesize)
                msg->bbox = dragbbox;
            else
            {
                msg->bbox.minx = msg->bbox.miny = 1;
                msg->bbox.maxx = msg->bbox.maxy = 0;
            }

            {
                int n = 0;
                do
                {
                    msg->filetypes[n] = dd.filetypes[n];
                } while (dd.filetypes[n++] != -1);
                msg->header.size = sizeof(MessageDraggingRec) + (n - 1) * sizeof(int);
            }
/* dprintf("Sending MESSAGE_DRAGGING to %d\n" _ dd.claimant); */
            if (dd.claimant != -1)
                return swi (Wimp_SendMessage,
                            R0, EV_USER_MESSAGE_RECORDED,
                            R1, msg,
                            R2, dd.claimant,  END);
            else
                return swi (Wimp_SendMessage,
                            R0, dragfinished ? EV_USER_MESSAGE_RECORDED : EV_USER_MESSAGE,
                            R1, msg,
                            R2, dd.mouse.windowhandle,
                            R3, dd.mouse.iconhandle,  END);
        }
        break;

    case EV_KEY_PRESSED:
        {
            KeyPressPtr key = (KeyPressPtr) buf;

            /* drag has priority over keyboard shortcuts */
            *consumed = (key->code != 0x1b);
        }
        break;

    case EV_USER_DRAG_BOX:
/* dprintf("Drag Finished; sending final msg...\n"); */
        dragfinished = TRUE;
        return dd_interactor (EV_NULL_REASON_CODE, buf, closure, consumed);
        break;

    case EV_USER_MESSAGE:                  
        {
            MessageDragClaimPtr msg = (MessageDragClaimPtr) buf;
            if (msg->header.messageid == MESSAGE_DRAG_CLAIM)
            {
/*                dprintf(" Received MESSAGE_DRAG_CLAIM "); */
                *consumed = TRUE;
/*                dprintf("Claimant=%d, ref %d, flags %d\n" _ msg->header.taskhandle _ msg->header.myref _ msg->flags); */
                if ((dd.claimantsflags & ~(msg->flags)) & BIT(0))
                    dragdrop_normal_pointer ();
                if ((dd.claimantsflags & ~(msg->flags)) & BIT(1))
                    do_dragbox (5, NULL, NULL);
                else if ((msg->flags & ~(dd.claimantsflags)) & BIT(1))
                    do_dragbox (7, NULL, NULL);
                dd.claimant = msg->header.taskhandle;
                dd.claimantsref = msg->header.myref;
                dd.claimantsflags = msg->flags;

                if (dragfinished)
                {
/* dprintf("DRAGFINISHED; calling callback\n"); */
                    if (dd.claimantsflags & BIT(0))
                        dragdrop_normal_pointer ();
                    interactor_remove ();
                    return (*callback) (closure, &dd);
                }
            }
        }
        break;
            
    case EV_USER_MESSAGE_ACKNOWLEDGE:
        {
            MessageDraggingPtr msg = (MessageDraggingPtr) buf;
            if (msg->header.messageid == MESSAGE_DRAGGING)
            {
/* dprintf("MESSAGE_DRAGGING BOUNCED: "); */
                *consumed = TRUE;
                if (dd.claimant != -1)
                {
/* dprintf("%s Claimaint == %d, resending to window/icon %d %d\n" _ dragfinished ? "DRAGFINISHED" : "" _ dd.claimant _ dd.mouse.windowhandle _ dd.mouse.iconhandle); */
                    if (dd.claimantsflags & BIT(0))
                        dragdrop_normal_pointer ();
                    if (dd.claimantsflags & BIT(1))
                        do_dragbox (5, NULL, NULL);
                    dd.claimant = -1;
                    dd.claimantsflags = 0;
                    dd.claimantsref = 0;
                    msg->header.yourref = 0;
                    return swi (Wimp_SendMessage,
                                R0, dragfinished ? EV_USER_MESSAGE_RECORDED : EV_USER_MESSAGE,
                                R1, msg,
                                R2, dd.mouse.windowhandle,
                                R3, dd.mouse.iconhandle,  END);
                }
                else
                {
/* dprintf("NO CLAIMANT "); */
                    if (dragfinished)
                    {
/* dprintf("DRAGFINISHED, calling callback\n"); */
                        interactor_remove ();
                        return (*callback) (closure, &dd);
                    }
/* dprintf("\n"); */
                }
            }
        }
        break;
default:
/* dprintf("Other %d\n" _ event); */
break;
    }

    return NULL;
}


/*
 * Start a drag-n-drop.  Handles the message protocol between sender
 * and receiver, and calls the sender back if the data needs to be
 * transmitted.  The 'filetypes' array must have a lifetime outside
 * this call - the interactor will be using it.  In practice this means
 * that it probably needs to be static.
 */
 
error * dragdrop_start (RectPtr bbox,           /* screen-relative, in OS Units */
                        Bool truesz,            /* TRUE -> size of bbox is data's real size */
                        unsigned int dragflags, /* as for Message_Dragging */
                        int *filetypes,         /* -1 terminated */
                        DragDropCallback cback,
                        void *closure,
                        char *spritename,
                        int numselected)
{
    interactor_cancel ();

    truesize = truesz;
    dd.dragflags = dragflags;
    dd.filetypes = filetypes;
    dd.claimant = -1;
    dd.claimantsflags = 0;
    dd.claimantsref = 0;
    dd.claimantstypes = NULL;

    dragfinished = FALSE;
    callback = cback;

    ER ( swi (Wimp_GetPointerInfo,  R1, &dd.mouse,  END) );
    dragbbox.minx = (bbox->minx - dd.mouse.position.x) * 400;
    dragbbox.maxx = (bbox->maxx - dd.mouse.position.x) * 400;
    dragbbox.miny = (bbox->miny - dd.mouse.position.y) * 400;
    dragbbox.maxy = (bbox->maxy - dd.mouse.position.y) * 400;

    /* added so we can be fancy and use a packet */
    ER ( do_dragbox (5, (int*) 1,
                        (numselected > 1) ? "package" : spritename) );

    interactor_install (dd_interactor, closure);
    interactor_enable_events (BIT(EV_NULL_REASON_CODE));
    interactor_set_timeout (5);
    return NULL;
}



/*
 ****************************************************************
 * RECEIVER
 ****************************************************************
 */


static int claimref = 0;        /* non-zero if we're claiming */
static int claimwin;            /* window handle if claiming */


/*
 * Notify client's receive code about Message_Dragging.  This is done
 * by the client supplying an extern function called dragdrop_accept.
 * The parameters of this function are a window handle, a pointer
 * to a MessageDragging event and a pointer to an int.  If the message
 * is claimed, this int will have been updated to hold the claimant's
 * reply's myref, zero otherwise.
 *
 * The function is called with msg == NULL to inform the client's code
 * that the operation is over (so it can undraw any feedback, etc).  
 */

static error * dragdrop_offer (int windowhandle, MessageDraggingPtr msg)
{
    claimref = 0;
    return dragdrop_accept (windowhandle, msg, &claimref);
}


/*
 * Return the claimant's reference, or 0 if unclaimed.
 */

int dragdrop_claimref ()
{
    return claimref;
}


int dragdrop_claimwin ()
{
    return claimwin;
}


/*
 * Sender can call this if it detects that the destination
 * is itself; it must then arrange for transfer to happen
 * internally rather than using DataSave.  Also used by
 * receiver if it is told that the sender is backing out
 * of the transaction.
 */

error * dragdrop_cancel ()
{
/* dprintf ("(Receive Cancelled)."); */
    if (claimref)
        return dragdrop_offer (claimwin, NULL);
    return NULL;
}


/*
 * Offer the Message_Dragging to the current claiming window,
 * and if that doesn't want it, offer it to the one mentioned in the
 * message.  Window-type-specific code gets called by dragdrop_offer
 * and is responsible for sending the Message_DragClaim.
 */

error * dragdrop_message_dragging (MessageDraggingPtr msg)
{
    if (msg->flags & BIT(4))
        return dragdrop_cancel();

    if (claimref)
    {
/* dprintf("**OFFERING TO OLD\n"); */
        ER ( dragdrop_offer (claimwin, msg) );
        if (claimref)
            return NULL;        /* claimed by current claimant */
    }
/* dprintf("**OFFERING TO NEW\n"); */
    ER (dragdrop_offer (msg->windowhandle, msg) );
    if (claimref)
    {
        claimwin = msg->windowhandle; /* new claimant */
    }
    return NULL;
}


/*
 ****************************************************************
 * AUTO-SCROLL SUPPORT
 ****************************************************************
 */


#define AUTOSCROLL_DELTA 20          /* Must move less than this much to trigger scroll */
#define AUTOSCROLL_MAX_MARGIN 80     /* Be within this distance of window edge to trigger scroll */
#define AUTOSCROLL_START_DELAY 10    /* Don't start scrolling for this many cs */
#define AUTOSCROLL_REPEAT_DELAY 10   /* Once scrolling take a step this often */


/*
 * Position is in screen coordinates and is known to be within the window.
 * Return TRUE if scrolling occurred, else FALSE.
 */

static Bool perhaps_scroll (WindowPtr window, PointPtr position)
{
    int vis, margin, dist;
    PointRec newoffset = window->scrolloffset;
    WindowRec state;

    state.handle = window->handle;
    swi (Wimp_GetWindowState,  R1, &state,  END);

    /* X */

    if (state.flags & WF_HSCROLL)
    {
        int work = window->workarea.maxx - window->workarea.minx;
        vis = window->visarea.maxx - window->visarea.minx;
        if (vis > work)
            vis = work;
        margin = vis / 4;
        if (margin > AUTOSCROLL_MAX_MARGIN)
            margin = AUTOSCROLL_MAX_MARGIN;

        dist = WIMP_ALIGN_COORD (position->x - 
                                 (window->visarea.maxx - margin));
        if (dist > 0)
        {
            newoffset.x += dist;
            if (newoffset.x > window->workarea.maxx - vis)
                newoffset.x = window->workarea.maxx - vis;
        }

        dist = WIMP_ALIGN_COORD (position->x -
                                 (window->visarea.minx + margin));
        if (dist < 0)
        {
            newoffset.x += dist;
            if (newoffset.x < window->workarea.minx)
                newoffset.x = window->workarea.minx;
        }
    }
    /* Y */

    if (state.flags & WF_VSCROLL)
    {
        vis = window->visarea.maxy - window->visarea.miny;
        margin = vis / 4;
        if (margin > AUTOSCROLL_MAX_MARGIN)
            margin = AUTOSCROLL_MAX_MARGIN;

        dist = WIMP_ALIGN_COORD (position->y -
                                 (window->visarea.maxy - margin));
        if (dist > 0)
        {
            newoffset.y += dist;
            if (newoffset.y > window->workarea.maxy)
                newoffset.y = window->workarea.maxy;
        }

        dist = WIMP_ALIGN_COORD (position->y -
                                 (window->visarea.miny + margin));
        if (dist < 0)
        {
            newoffset.y += dist;
            if (newoffset.y < window->workarea.miny + vis)
                newoffset.y = window->workarea.miny + vis;
        }
    }


    /*
     * The scroll distance must either be negative - or must be greater than
     *  or equal to a pixel size - in order that scrolling will actually take
     *  place; so if neither of these conditions are met, we set the scroll
     *  distance to zero to avoid generating any OPEN_WINDOW_REQUEST.
     */

    dist = newoffset.x - window->scrolloffset.x;
    if (dist > 0 && dist < scalex)
        newoffset.x = window->scrolloffset.x;
    dist = newoffset.y - window->scrolloffset.y;
    if (dist > 0 && dist < scaley)
        newoffset.y = window->scrolloffset.y;


    if (newoffset.x != window->scrolloffset.x ||
        newoffset.y != window->scrolloffset.y)
    {
        state.scrolloffset = window->scrolloffset = newoffset;
        window->behind = state.behind;
        (void) swi (Wimp_SendMessage,  R0, EV_OPEN_WINDOW_REQUEST,  R1, &state,  R2, state.handle,  END);
        return TRUE;
    }
    return FALSE;
}


#include <math.h>

static int distance (PointPtr a, PointPtr b)
{
    int dx = a->x - b->x, dy = a->y - b->y;
    /* MAX(abs(dx), abs(dy)) would probably do, really */
    return (int) sqrt ((double) ((dx * dx) + (dy * dy)));
}


/*
 * Consider auto-scrolling the window.  Position is the screen coordinates
 * of the mouse.  Returns TRUE if the window was actually scrolled.
 * Sets *removeptr to TRUE if the caller should reset the pointer
 * shape to the normal one (but caller should only do this if it's
 * been changed!)
 */

Bool dragdrop_scroll (WindowPtr win, PointPtr position, Bool *removeptr)
{
    static Bool mouseposvalid = FALSE;
    static PointRec mousepos;
    static unsigned int nexttime;
    Bool scrolled = FALSE;

    if (win == NULL || wimp_read_modifiers () & MODIFIER_ALT)
    {
        mouseposvalid = FALSE;
        if (removeptr) *removeptr = TRUE;
        return NULL;
    }

    *removeptr = FALSE;

    /* Decide if auto-scroll might be appropriate */
    if (mouseposvalid)
    {
        unsigned int now;
        (void) swi (OS_ReadMonotonicTime,  OUT,  R0, &now,  END);
        if (now > nexttime)
        {
            if (distance(&mousepos, position) < AUTOSCROLL_DELTA)
            {
                if (perhaps_scroll (win, position))
                {
                    nexttime = now + AUTOSCROLL_REPEAT_DELAY;
                    mousepos = *position;
                    scrolled = TRUE;
                }
                else
                {
                    mouseposvalid = FALSE;
                    if (removeptr) *removeptr = TRUE;
                }
            }
            else
            {
                mouseposvalid = FALSE;
                if (removeptr) *removeptr = TRUE;
            }
        }
    }
    else
    {
        mouseposvalid = TRUE;
        mousepos = *position;
        (void) swi (OS_ReadMonotonicTime,  OUT,  R0, &nexttime,  END);
        nexttime += AUTOSCROLL_START_DELAY;
    }

    return scrolled;
}





